标准 Hero

在 Android 中有个词儿叫过渡动画,不同的 Activity 通过为控件设置相同 android:transitionName 来进行页面之间的控件传递效果。

在 Flutter 中也有类似的功能,那就是 hero,它的使用方法也很简单,总结起来就两点:

  1. 使用 Hero ”包裹“要过渡的 Widget
  2. 给两个 Route 之间过渡的 Widget 设置相同的 tag

so easy~

看代码:

void main() {
  runApp(MaterialApp(
    routes: {
      "/": (BuildContext context) => new HeroTestFirstRoute(),
      "/Second": (BuildContext context) => new HeroTestSecondRoute(),
    },
    initialRoute: "/",
  ));
}

class HeroTestFirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hero First"),
      ),
      body: Container(
        alignment: Alignment.bottomCenter,
        color: Colors.lightGreenAccent,
        child: GestureDetector(
          onTap: () {
            Navigator.pushNamed(context, "/Second");
          },
          child: Hero(
            tag: "chenshu",
            child: Image.asset(
              "assets/images/chenshu.jpg",
              width: 100,
              height: 100,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

class HeroTestSecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hero Second"),
      ),
      body: Container(
        alignment: Alignment.topCenter,
        color: Colors.yellow,
        child: GestureDetector(
          onTap: () {
            Navigator.pop(context);
          },
          child: Hero(
            tag: "chenshu",
            child: Image.asset(
              "assets/images/chenshu.jpg",
              width: 400,
              height: 500,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

效果如下:

在 Route 跳转的过程中,Flutter 会自动计算出一个 RectTween(还记得之前讲的 Tween 么),这个 RectTween 就定义了从源 Route 到目标 Route 过程中,Hero 的边界。

我们来看一下 Hero 的构造方法:

  const Hero({
    Key key,
    @required this.tag,
    this.createRectTween,
    this.flightShuttleBuilder,
    this.placeholderBuilder,
    this.transitionOnUserGestures = false,
    @required this.child,
  })
  • tag,必填,Flutter 系统就是根据 tag 来确定新旧 Route 之间的 widget 的对应关系的。
  • createRectTween,在动画进行中Flutter是依靠Tween来实现,通过createRectTween属性把Tween传给Hero。系统为我们提供了默认的 MeterialRectArcTween 曲线路径
  • flightShuttleBuilder,在过渡飞行期间,代替飞行 Widget 的组件
  • placeholderBuilder,设置占位符,在组件飞离它曾经处于的位置并且到达目标位置之前,目标处有一处空的地方。 我们可以在此位置添加占位符。
  • transitionOnUserGestures,使 hero 动画可以支持 iOS 返回滑动手势,源 Route 和目标 Route 都设置为 true 即可。
  • child,页面之间过渡的 Widget。

切记,同一个 Route 里的 Hero tag 不能重复,源 Route 和目标 Route 中的 tag 必须一一对应。

下面几张图说明了 Widget 是如何在 Route 之间过渡的:

  1. 过渡之前,源 hero 会在源路由的 widget 树中等待。目标路由尚不存在,叠加层为空。

  2. 当转跳开始触发时,会执行以下操作:

    1. 使用 Material motion 规范中所述的曲线运动计算目标 hero 的路径。现在Flutter知道 hero 在哪里结束。
    2. 将目标 hero 放置在叠加层中,与源 hero 的位置和大小相同。将 hero 添加到叠加层会更改其 Z 序,以使其出现在所有路由的顶部
    3. 将源 hero 移出路由。

  3. 当 hero 在 Route 之间“飞行”时,其矩形边界使用 hero 中 createRectTween 属性中指定的 Tween 进行动画。默认情况下,Flutter使用 MaterialRectArcTween 的一个实例,该实例沿曲线路径对矩形的对角进行动画处理。

  4. 当飞行完成时:

    1. Flutter 将 hero widget 从叠加层移动到目标路由。叠加层现在是空的。
    2. 目标 hero 出现在目标路由的最终位置。
    3. 源 hero 恢复到其路由。


径向 Hero

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'dart:math' as math;

void main() => runApp(MaterialApp(home: RadialExpansionDemo()));

class Photo extends StatelessWidget {
  final String photo;
  final VoidCallback onTap;
  final double width;

  const Photo({Key key, this.photo, this.onTap, this.width}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Theme.of(context).primaryColor.withOpacity(0.25),
      child: InkWell(
        onTap: onTap,
        child: LayoutBuilder(builder: (context, size) {
          return Image.asset(photo, fit: BoxFit.contain);
        }),
      ),
    );
  }
}

class RadialExpansionDemo extends StatelessWidget {
  static const double kMinRadius = 32.0;
  static const double kMaxRadius = 128.0;
  static const opacityCurve =
      const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);

  static RectTween _createRectTween(Rect begin, Rect end) {
    return MaterialRectArcTween(begin: begin, end: end);
  }

  static Widget _buildPage(
      BuildContext context, String imageName, String description) {
    return Container(
      color: Theme.of(context).canvasColor,
      child: Center(
        child: Card(
          elevation: 8,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SizedBox(
                width: kMaxRadius * 2,
                height: kMaxRadius * 2,
                child: Hero(
                  createRectTween: _createRectTween,
                  tag: imageName,
                  child: RadialExpansion(
                    maxRadius: kMaxRadius,
                    child: Photo(
                      photo: imageName,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                ),
              ),
              Text(description,
                  style: TextStyle(fontWeight: FontWeight.bold),
                  textScaleFactor: 3.0),
              const SizedBox(height: 16)
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHero(
      BuildContext context, String imageName, String description) {
    return Container(
        width: kMinRadius * 2,
        height: kMinRadius * 2,
        child: Hero(
          createRectTween: _createRectTween,
          tag: imageName,
          child: RadialExpansion(
            maxRadius: kMaxRadius,
            child: Photo(
                photo: imageName,
                onTap: () {
                  Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder:
                      (BuildContext context, Animation<double> animation,
                          Animation<double> secondaryAnimation) {
                    return AnimatedBuilder(
                        animation: animation,
                        builder: (context, child) {
                          return Opacity(
                            opacity: opacityCurve.transform(animation.value),
                            child: _buildPage(context, imageName, description),
                          );
                        });
                  }));
                }),
          ),
        ));
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 5.0;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Radial Transition Demo'),
      ),
      body: Container(
        padding: EdgeInsets.all(32),
        alignment: FractionalOffset.bottomLeft,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            _buildHero(context, 'assets/images/chair-alpha.png', "Chair"),
            _buildHero(
                context, 'assets/images/binoculars-alpha.png', "Binoculars"),
            _buildHero(
                context, 'assets/images/beachball-alpha.png', "Beach ball"),
          ],
        ),
      ),
    );
  }
}

class RadialExpansion extends StatelessWidget {
  final double maxRadius;
  final clipRectSize;
  final Widget child;

  const RadialExpansion({Key key, this.maxRadius, this.child})
      : clipRectSize = 2.0 * (maxRadius / math.sqrt2),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect(
            child: child,
          ),
        ),
      ),
    );
  }
}

代码解释看这里:

Radial hero 动画

动画Animation开发指南-Hero动画2

results matching ""

    No results matching ""